package language;

import compiler.Func;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import static java.util.Collections.EMPTY_MAP;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class LArray<T extends LType> extends LType implements Comparable<LArray<T>>, Iterable<T> {

    public static final LArray<?> EMPTY = LArray.of(Collections.emptyList());

    private final List<T> list;

    private LArray(List<T> list) {
        this.list = Collections.unmodifiableList(new ArrayList(list));
    }

    public static <T extends LType> LArray<T> of(List<T> list) {
        return new LArray(new ArrayList(list));
    }

    public static <T extends LType> LArray<T> of(T... list) {
        return new LArray(Arrays.asList(list));
    }

    public static LArray<LCharacter> of(String str) {
        ArrayList list = new ArrayList();
        for (char c : str.toCharArray()) {
            list.add(new LCharacter(c));
        }
        return new LArray(list);
    }

    public static LArray<? extends LMagnitude> range(LMagnitude endExclusive) {
        if (endExclusive.compareTo(LNumber.ZERO) < 0) {
            return range(endExclusive.add(LNumber.ONE), LNumber.ONE).reverse();
        }
        return range(new LMagnitude(LNumber.ZERO, endExclusive.isChar()), endExclusive);
    }

    public static LArray<? extends LMagnitude> range(LMagnitude startInclusive, LMagnitude endExclusive) {
        long max = endExclusive.toLong();
        List<LMagnitude> list = new ArrayList();
        for (long i = startInclusive.toLong(); i < max; i++) {
            if (startInclusive.isChar()) {
                list.add(new LCharacter((char) i));
            } else {
                list.add(new LNumber(i));
            }
        }
        return LArray.of(list);
    }

    public LArray<? extends LType> flatten() {
        List<LType> copy = new ArrayList();
        stream().forEach(t -> {
            if (t instanceof LArray) {
                copy.addAll(((LArray) t).flatten().list);
            } else {
                copy.add(t);
            }
        });
        return LArray.of(copy);
    }

    public LArray<T> unique() {
        return LArray.of(stream().distinct().collect(Collectors.toList()));
    }

    public LType get(int index) {
        int size = list.size();
        if (size == 0) {
            return LNumber.ZERO;
        }
        if (index < 0) {
            index = size + index % size;
        }
        return list.get(index % size);
    }

    public LArray<? extends LType> set(int index, LType value) {
        int size = list.size();
        if (size == 0) {
            return this;
        }
        List<LType> copy = new ArrayList(list);
        if (index < 0) {
            index = size + index % size;
        }
        copy.set(index % size, value);
        return new LArray(copy);
    }

    public LArray append(LArray add) {
        ArrayList copy = new ArrayList(list);
        copy.addAll(add.list);
        return new LArray(copy);
    }

    public LArray insertAt(int index, LType insert) {
        ArrayList copy = new ArrayList(list);
        copy.add(index, insert);
        return new LArray(copy);
    }

    public LArray insertAtEnd(LType insert) {
        ArrayList copy = new ArrayList(list);
        copy.add(insert);
        return new LArray(copy);
    }

    public LArray<T> remove(LType remove) {
        List<T> copy = new ArrayList(list);
        copy.removeIf(remove::equals);
        return new LArray(copy);
    }

    public LArray<T> removeAll(LArray<?> remove) {
        List<T> copy = new ArrayList(list);
        copy.removeAll(remove.list);
        return new LArray(copy);
    }

    public LArray<T> repeat(LMagnitude n) {
        List<T> newL = new ArrayList();
        List<T> oldL = list();
        for (int i = 0; i < n.toInt(); i++) {
            newL.addAll(oldL);
        }
        double fraction = (n.toDouble() - n.toInt()) * size();
        for (int i = 0; i < fraction; i++) {
            newL.add((T) get(i));
        }
        return LArray.of(newL);
    }

    public LArray<? extends LType> replace(LType search, LType replacement) {
        ArrayList<LType> copy = new ArrayList(list);
        for (int i = 0; i < copy.size(); i++) {
            if (copy.get(i).equals(search)) {
                copy.set(i, replacement);
            }
        }
        return LArray.of(copy);
    }

    public LArray<? extends LType> replaceAll(LArray search, LType replacement) {
        ArrayList<LType> copy = new ArrayList(list);
        for (int i = 0; i < copy.size(); i++) {
            if (search.list.contains(copy.get(i))) {
                copy.set(i, replacement);
            }
        }
        return LArray.of(copy);
    }

    public LArray<? extends LType> replaceWhere(Func function, LType replacement) {
        ArrayList<LType> copy = new ArrayList(list);
        for (int i = 0; i < copy.size(); i++) {
            if (function.accept(copy.get(i), EMPTY_MAP).toBoolean()) {
                copy.set(i, replacement);
            }
        }
        return LArray.of(copy);
    }

    public LArray<? extends T> filter(Func function) {
        return LArray.of(stream().filter(t -> function.accept(t, EMPTY_MAP).toBoolean()).collect(Collectors.toList()));
    }

    public LArray<? extends T> intersection(LArray<?> that) {
        ArrayList<?> copyThat = new ArrayList(that.list);
        ArrayList<T> intersection = new ArrayList();
        stream().filter(copyThat::remove).forEach(intersection::add);
        return new LArray(intersection);
    }

    public LArray<? super T> union(LArray<?> that) {
        ArrayList<? extends LType> copyThat = new ArrayList(that.list);
        ArrayList<LType> union = new ArrayList();
        stream().forEach(o -> {
            union.add(o);
            copyThat.remove(o);
        });
        union.addAll(copyThat);
        return new LArray(union);
    }

    public LArray<? super T> difference(LArray<?> that) {
        ArrayList<?> copyThat = new ArrayList(that.list);
        ArrayList<T> difference = new ArrayList();
        stream().filter(o -> !copyThat.remove(o)).forEach(difference::add);
        return new LArray(difference);
    }

    public LArray<? super T> symmetricDifference(LArray<?> that) {
        ArrayList<? extends LType> copyThat = new ArrayList(that.list);
        ArrayList<LType> difference = new ArrayList();
        stream().filter(t -> !copyThat.remove(t)).forEach(difference::add);
        difference.addAll(copyThat);
        return new LArray(difference);
    }

    public LArray<?> leftJoin(LArray<?> that, Func function) {
        List newList = new ArrayList();
        for (int i = 0; i < size(); i++) {
            newList.add(((Func) function.accept(this.get(i), EMPTY_MAP)).accept(that.get(i), EMPTY_MAP));
        }
        return LArray.of(newList);
    }

    public <U extends LType> LArray<?> crossJoin(LArray<U> that, Func function) {
        List newList = new ArrayList();
        for (T t : this) {
            for (U u : that) {
                newList.add(((Func) function.accept(t, EMPTY_MAP)).accept(u, EMPTY_MAP));
            }
        }
        return LArray.of(newList);
    }

    public LArray<T> subList(int startInclusive, int endExclusive) {
        List copy = new ArrayList();
        for (int i = startInclusive; i < endExclusive; i++) {
            copy.add(list.get(i));
        }
        return new LArray(copy);
    }

    public LArray<T> rotate(int amount) {
        int size = list.size();
        if (amount < 0) {
            amount = size + amount % size;
        }
        List copy = new ArrayList(list);
        for (int i = 0; i < amount % size; i++) {
            copy.add(copy.remove(0));
        }
        return new LArray(copy);
    }

    public LArray<?> joinOnArray(LArray<?> join) {
        if (list.isEmpty()) {
            return this;
        }
        List<LType> copy = new ArrayList();
        for (int i = 0; i < size() - 1; i++) {
            copy.addAll(((LArray) get(i)).list);
            copy.addAll(join.list);
        }
        copy.addAll(((LArray) get(size() - 1)).list);
        return LArray.of(copy);
    }

    public LArray<LArray<T>> splitOnArray(LArray<?> split) {
        List<LArray<T>> copy = new ArrayList();
        List<T> temp = new ArrayList();
        for (int i = 0; i < size(); i++) {
            boolean match = true;
            for (int j = 0; j < split.size(); j++) {
                if (!get(i + j).equals(split.get(j))) {
                    match = false;
                }
            }
            if (match) {
                i += split.size() - 1;
                copy.add(LArray.of(temp));
                temp.clear();
            } else {
                temp.add((T) get(i));
            }
        }
        copy.add(LArray.of(temp));
        return LArray.of(copy);
    }

    public LArray<T> shuffle() {
        List<T> copy = new ArrayList(list);
        Collections.shuffle(copy);
        return LArray.of(copy);
    }

    public LArray<T> reverse() {
        List<T> newList = new ArrayList();
        for (int i = list.size() - 1; i >= 0; i--) {
            newList.add((T) get(i));
        }
        return LArray.of(newList);
    }

    public LArray<T> sort() {
        return sort(LFunction.of(l -> {
            if (l instanceof LMagnitude) {
                return LFunction.of(m -> {
                    if (m instanceof LMagnitude) {
                        return new LNumber(((LMagnitude) l).compareTo(((LMagnitude) m)));
                    }
                    throw new RuntimeException("Array is not sortable.");
                });
            }
            if (l instanceof LArray) {
                return LFunction.of(m -> {
                    if (m instanceof LArray) {
                        return new LNumber(((LArray) l).compareTo(((LArray) m)));
                    }
                    throw new RuntimeException("Array is not sortable.");
                });
            }
            throw new RuntimeException("Array is not sortable.");
        }));
    }

    public LArray<T> sort(Func function) {
        List<T> copy = new ArrayList(list);
        Collections.sort(copy, (t1, t2) -> {
            LType l1 = function.accept(t1, EMPTY_MAP);
            LType l2 = function.accept(t2, EMPTY_MAP);
            if (l1 instanceof LMagnitude) {
                if (l2 instanceof LMagnitude) {
                    return ((LMagnitude) l1).compareTo(((LMagnitude) l2));
                }
                throw new RuntimeException("Array is not sortable.");
            }
            if (l1 instanceof LArray) {
                if (l2 instanceof LArray) {
                    return ((LArray) l1).compareTo(((LArray) l2));
                }
                throw new RuntimeException("Array is not sortable.");
            }
            throw new RuntimeException("Array is not sortable.");
        });
        return LArray.of(copy);
    }

    public LArray<?> map(Func func, Map<Character, LType> vars) {
        List<LType> copy = new ArrayList();
        list.forEach(t -> copy.add(func.accept(t, vars)));
        return new LArray(copy);
    }

    public <R extends LType & Func> LArray<R> map2(Func func, Map<Character, LType> vars) {
        List<LFunction> copy = new ArrayList();
        list.forEach(t -> copy.add(LFunction.of(l -> ((Func) func.accept(l, vars)).accept(t, vars))));
        return new LArray(copy);
    }

    public <R extends LType> LArray<R> eachOnEach(LArray<?> arr) {
        List newList = new ArrayList();
        for (int i = 0; i < arr.size(); i++) {
            newList.add(((Func) get(i)).accept(arr.get(i), EMPTY_MAP));
        }
        return LArray.of(newList);
    }

    public LType applyAllTo(LType l) {
        for (Func f : (LArray<? extends Func>) this) {
            l = f.accept(l, EMPTY_MAP);
        }
        return l;
    }

    public LType reduce(Func func) {
        return ((Stream<LType>) stream()).reduce((t1, t2) -> ((Func) func.accept(t1, null)).accept(t2, null)).orElse(LNumber.ZERO);
    }

    public T reduce(Func func, T identity) {
        return stream().reduce(identity, (t1, t2) -> (T) ((Func) func.accept(t1, null)).accept(t2, null));
    }

    @Override
    public String toString() {
        if (stream().allMatch(c -> c instanceof LCharacter)) {
            char[] chars = new char[list.size()];
            for (int i = 0; i < list.size(); i++) {
                chars[i] = ((LCharacter) list.get(i)).toChar();
            }
            return new String(chars);
        }
        return stream().map(LType::toString).collect(Collectors.joining());
    }

    @Override
    public String toDebugString() {
        if (stream().allMatch(c -> c instanceof LCharacter)) {
            char[] chars = new char[list.size()];
            for (int i = 0; i < list.size(); i++) {
                chars[i] = (char) ((LCharacter) list.get(i)).toDouble();
            }
            return '"' + new String(chars) + '"';
        }
        return '{' + stream().map(LType::toDebugString).collect(Collectors.joining(", ")) + '}';
    }

    public List<T> list() {
        return new ArrayList(list);
    }

    public int size() {
        return list.size();
    }

    public int indexOf(LType l) {
        return list.indexOf(l);
    }

    public int count(LType l) {
        return (int) stream().filter(l::equals).count();
    }

    @Override
    public boolean toBoolean() {
        return !list.isEmpty();
    }

    @Override
    public int compareTo(LArray<T> that) {
        int i;
        for (i = 0; i < size(); i++) {
            if (i >= that.size()) {
                return 1;
            }
            LType get1 = this.get(i);
            LType get2 = that.get(i);
            if (get1 instanceof LMagnitude) {
                if (get2 instanceof LMagnitude) {
                    int compare = ((LMagnitude) get1).compareTo((LMagnitude) get2);
                    if (compare != 0) {
                        return compare;
                    }
                } else {
                    throwArrayCompareTypeException(get1, get2);
                }
            } else if (get1 instanceof LArray) {
                if (get2 instanceof LArray) {
                    int compare = ((LArray) get1).compareTo((LArray) get2);
                    if (compare != 0) {
                        return compare;
                    }
                } else {
                    throwArrayCompareTypeException(get1, get2);
                }
            } else {
                throwArrayCompareTypeException(get1, get2);
            }
        }
        return i < that.size() ? -1 : 0;
    }

    @Override
    public boolean equals(Object o) {
        if (o == null) {
            return false;
        }
        if (!o.getClass().equals(getClass())) {
            return false;
        }
        LArray that = (LArray) o;
        return this.list.equals(that.list);
    }

    @Override
    public int hashCode() {
        int hash = 5;
        hash = 41 * hash + list.hashCode();
        return hash;
    }

    public Stream<T> stream() {
        return list.stream();
    }

    private void throwArrayCompareTypeException(LType get1, LType get2) {
        throw new RuntimeException("Incomparable types in arrays: "
                                   + get1.getClass() + " and " + get2.getClass() + ".");
    }

    @Override
    public Iterator<T> iterator() {
        Iterator<T> it = list.iterator();
        return new Iterator<T>() {
            @Override
            public boolean hasNext() {
                return it.hasNext();
            }

            @Override
            public T next() {
                return it.next();
            }
        };
    }
}
